package com.nd.android.musicboxlibrary.util;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.text.DateFormatSymbols;
import java.text.FieldPosition;
import java.text.Format;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;

import com.nd.android.musicboxlibrary.business.comm.MagicNumber;
import com.nd.android.musicboxlibrary.util.fastDateUtil.CharacterLiteral;
import com.nd.android.musicboxlibrary.util.fastDateUtil.NumberRule;
import com.nd.android.musicboxlibrary.util.fastDateUtil.PaddedNumberField;
import com.nd.android.musicboxlibrary.util.fastDateUtil.Rule;
import com.nd.android.musicboxlibrary.util.fastDateUtil.StringLiteral;
import com.nd.android.musicboxlibrary.util.fastDateUtil.TextField;
import com.nd.android.musicboxlibrary.util.fastDateUtil.TwelveHourField;
import com.nd.android.musicboxlibrary.util.fastDateUtil.TwentyFourHourField;
import com.nd.android.musicboxlibrary.util.fastDateUtil.TwoDigitMonthField;
import com.nd.android.musicboxlibrary.util.fastDateUtil.TwoDigitNumberField;
import com.nd.android.musicboxlibrary.util.fastDateUtil.TwoDigitYearField;
import com.nd.android.musicboxlibrary.util.fastDateUtil.UnpaddedMonthField;
import com.nd.android.musicboxlibrary.util.fastDateUtil.UnpaddedNumberField;

/**
 * <p>
 * FastDateFormat is a fast and thread-safe version of {@link java.text.SimpleDateFormat}.
 * </p>
 * 
 * @author Apache Software Foundation
 * @version $Id: FastDateFormat.java 892161 2009-12-18 07:21:10Z bayard $
 * @since 2.0
 */
public class FastDateFormat extends Format implements MagicNumber {
    
    /**
     * 
     * <br>Created 2014年11月28日 上午12:05:46
     * @author       wat.ztag
    */
    @Override
    public void initMagicNumber() {
    }
    
    /**
     * Required for serialization support.
     * 
     * @see java.io.Serializable
     */
    private static final long serialVersionUID = 1L;

    // @GuardedBy("this")
    private static String cDefaultPattern; // lazily initialised by
    // getInstance()

    private static final Map<FastDateFormat, FastDateFormat> cInstanceCache = new HashMap<FastDateFormat, FastDateFormat>(
            7);

    private static final Map<Object, FastDateFormat> cDateInstanceCache = new HashMap<Object, FastDateFormat>(
            7);

    private static final Map<Object, FastDateFormat> cTimeInstanceCache = new HashMap<Object, FastDateFormat>(
            7);

    /**
     * The pattern.
     */
    private final String mPattern;

    /**
     * The time zone.
     */
    private final TimeZone mTimeZone;

    /**
     * Whether the time zone overrides any on Calendars.
     */
    private final boolean mTimeZoneForced;

    /**
     * The locale.
     */
    private final Locale mLocale;

    /**
     * Whether the locale overrides the default.
     */
    private final boolean mLocaleForced;

    /**
     * The parsed rules.
     */
    private transient Rule[] mRules;

    /**
     * The estimated maximum length.
     */
    private transient int mMaxLengthEstimate;

    /**
     * <p>
     * Gets a formatter instance using the default pattern in the default locale.
     * </p>
     * 
     * @return a date/time formatter
     */
    public static FastDateFormat getInstance() {
        return getInstance(getDefaultPattern(), null, null);
    }

    /**
     * <p>
     * Gets a formatter instance using the specified pattern in the default locale.
     * </p>
     * 
     * @param pattern
     *            {@link java.text.SimpleDateFormat} compatible pattern
     * @return a pattern based date/time formatter if pattern is invalid
     */
    public static FastDateFormat getInstance(String pattern) {
        return getInstance(pattern, null, null);
    }

    /**
     * <p>
     * Gets a formatter instance using the specified pattern and time zone.
     * </p>
     * 
     * @param pattern
     *            {@link java.text.SimpleDateFormat} compatible pattern
     * @param timeZone
     *            optional time zone, overrides time zone of formatted date
     * @return a pattern based date/time formatter if pattern is invalid
     */
    public static FastDateFormat getInstance(String pattern, TimeZone timeZone) {
        return getInstance(pattern, timeZone, null);
    }

    /**
     * <p>
     * Gets a formatter instance using the specified pattern and locale.
     * </p>
     * 
     * @param pattern
     *            {@link java.text.SimpleDateFormat} compatible pattern
     * @param locale
     *            optional locale, overrides system locale
     * @return a pattern based date/time formatter if pattern is invalid
     */
    public static FastDateFormat getInstance(String pattern, Locale locale) {
        return getInstance(pattern, null, locale);
    }

    /**
     * <p>
     * Gets a formatter instance using the specified pattern, time zone and locale.
     * </p>
     * 
     * @param pattern
     *            {@link java.text.SimpleDateFormat} compatible pattern
     * @param timeZone
     *            optional time zone, overrides time zone of formatted date
     * @param locale
     *            optional locale, overrides system locale
     * @return a pattern based date/time formatter if pattern is invalid or <code>null</code>
     */
    public static synchronized FastDateFormat getInstance(String pattern, TimeZone timeZone,
            Locale locale) {
        FastDateFormat emptyFormat = new FastDateFormat(pattern, timeZone, locale);
        FastDateFormat format = cInstanceCache.get(emptyFormat);
        if (format == null) {
            format = emptyFormat;
            format.init(); // convert shell format into usable one
            cInstanceCache.put(format, format); // this is OK!
        }
        return format;
    }

    /**
     * <p>
     * Gets the default pattern.
     * </p>
     * 
     * @return the default pattern
     */
    private static synchronized String getDefaultPattern() {
        if (cDefaultPattern == null) {
            cDefaultPattern = new SimpleDateFormat().toPattern();
        }
        return cDefaultPattern;
    }

    /**
     * <p>
     * Constructs a new FastDateFormat.
     * </p>
     * 
     * @param pattern
     *            {@link java.text.SimpleDateFormat} compatible pattern
     * @param timeZone
     *            time zone to use, <code>null</code> means use default for <code>Date</code> and
     *            value within for <code>Calendar</code>
     * @param locale
     *            locale, <code>null</code> means use system default if pattern is invalid or
     *            <code>null</code>
     */
    protected FastDateFormat(String pattern, TimeZone timeZone, Locale locale) {
        super();
        if (pattern == null) {
            throw new IllegalArgumentException("The pattern must not be null");
        }
        mPattern = pattern;

        mTimeZoneForced = (timeZone != null);
        if (timeZone == null) {
            timeZone = TimeZone.getDefault();
        }
        mTimeZone = timeZone;

        mLocaleForced = (locale != null);
        if (locale == null) {
            locale = Locale.getDefault();
        }
        mLocale = locale;
    }

    /**
     * <p>
     * Initializes the instance for first use.
     * </p>
     */
    protected void init() {
        List<Rule> rulesList = parsePattern();
        mRules = rulesList.toArray(new Rule[rulesList.size()]);

        int len = 0;
        for (int i = mRules.length; --i >= 0;) {
            len += mRules[i].estimateLength();
        }

        mMaxLengthEstimate = len;
    }

    /**
     * <p>
     * Returns a list of Rules given a pattern.
     * </p>
     * 
     * @return a <code>List</code> of Rule objects if pattern is invalid
     */
    protected List<Rule> parsePattern() {
        DateFormatSymbols symbols = new DateFormatSymbols(mLocale);
        List<Rule> rules = new ArrayList<Rule>();
        String[] ERAs = symbols.getEras();
        String[] months = symbols.getMonths();
        String[] shortMonths = symbols.getShortMonths();
        String[] weekdays = symbols.getWeekdays();
        String[] shortWeekdays = symbols.getShortWeekdays();
        String[] AmPmStrings = symbols.getAmPmStrings();
        int length = mPattern.length();
        int[] indexRef = new int[1];
        for (int i = 0; i < length; i++) {
            indexRef[0] = i;
            String token = parseToken(mPattern, indexRef);
            i = indexRef[0];
            int tokenLen = token.length();
            if (tokenLen == 0) {
                break;
            }
            Rule rule;
            char c = token.charAt(0);
            switch (c) {
            case 'G': // era designator (text)
                rule = new TextField(Calendar.ERA, ERAs);
                break;
            case 'y': // year (number)
                if (tokenLen >= n4) {
                    rule = selectNumberRule(Calendar.YEAR, tokenLen);
                } else {
                    rule = TwoDigitYearField.INSTANCE;
                }
                break;
            case 'M': // month in year (text and number)
                if (tokenLen >= n4) {
                    rule = new TextField(Calendar.MONTH, months);
                } else if (tokenLen == n3) {
                    rule = new TextField(Calendar.MONTH, shortMonths);
                } else if (tokenLen == 2) {
                    rule = TwoDigitMonthField.INSTANCE;
                } else {
                    rule = UnpaddedMonthField.INSTANCE;
                }
                break;
            case 'd': // day in month (number)
                rule = selectNumberRule(Calendar.DAY_OF_MONTH, tokenLen);
                break;
            case 'h': // hour in am/pm (number, 1..12)
                rule = new TwelveHourField(selectNumberRule(Calendar.HOUR, tokenLen));
                break;
            case 'H': // hour in day (number, 0..23)
                rule = selectNumberRule(Calendar.HOUR_OF_DAY, tokenLen);
                break;
            case 'm': // minute in hour (number)
                rule = selectNumberRule(Calendar.MINUTE, tokenLen);
                break;
            case 's': // second in minute (number)
                rule = selectNumberRule(Calendar.SECOND, tokenLen);
                break;
            case 'S': // millisecond (number)
                rule = selectNumberRule(Calendar.MILLISECOND, tokenLen);
                break;
            case 'E': // day in week (text)
                rule = new TextField(Calendar.DAY_OF_WEEK, tokenLen < n4 ? shortWeekdays : weekdays);
                break;
            case 'D': // day in year (number)
                rule = selectNumberRule(Calendar.DAY_OF_YEAR, tokenLen);
                break;
            case 'F': // day of week in month (number)
                rule = selectNumberRule(Calendar.DAY_OF_WEEK_IN_MONTH, tokenLen);
                break;
            case 'w': // week in year (number)
                rule = selectNumberRule(Calendar.WEEK_OF_YEAR, tokenLen);
                break;
            case 'W': // week in month (number)
                rule = selectNumberRule(Calendar.WEEK_OF_MONTH, tokenLen);
                break;
            case 'a': // am/pm marker (text)
                rule = new TextField(Calendar.AM_PM, AmPmStrings);
                break;
            case 'k': // hour in day (1..24)
                rule = new TwentyFourHourField(selectNumberRule(Calendar.HOUR_OF_DAY, tokenLen));
                break;
            case 'K': // hour in am/pm (0..11)
                rule = selectNumberRule(Calendar.HOUR, tokenLen);
                break;
            case '\'': // literal text
                String sub = token.substring(1);
                if (sub.length() == 1) {
                    rule = new CharacterLiteral(sub.charAt(0));
                } else {
                    rule = new StringLiteral(sub);
                }
                break;
            default:
                throw new IllegalArgumentException("Illegal pattern component: " + token);
            }
            rules.add(rule);
        }
        return rules;
    }

    /**
     * <p>
     * Performs the parsing of tokens.
     * </p>
     * 
     * @param pattern
     *            the pattern
     * @param indexRef
     *            index references
     * @return parsed token
     */
    protected String parseToken(String pattern, int[] indexRef) {
        StringBuilder buf = new StringBuilder();

        int i = indexRef[0];
        int length = pattern.length();

        char c = pattern.charAt(i);
        if (c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z') {
            // Scan a run of the same character, which indicates a time
            // pattern.
            buf.append(c);

            while (i + 1 < length) {
                char peek = pattern.charAt(i + 1);
                if (peek == c) {
                    buf.append(c);
                    i++;
                } else {
                    break;
                }
            }
        } else {
            // This will identify token as text.
            buf.append('\'');

            boolean inLiteral = false;

            for (; i < length; i++) {
                c = pattern.charAt(i);

                if (c == '\'') {
                    if (i + 1 < length && pattern.charAt(i + 1) == '\'') {
                        // '' is treated as escaped '
                        i++;
                        buf.append(c);
                    } else {
                        inLiteral = !inLiteral;
                    }
                } else if (!inLiteral && (c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z')) {
                    i--;
                    break;
                } else {
                    buf.append(c);
                }
            }
        }

        indexRef[0] = i;
        return buf.toString();
    }

    /**
     * <p>
     * Gets an appropriate rule for the padding required.
     * </p>
     * 
     * @param field
     *            the field to get a rule for
     * @param padding
     *            the padding required
     * @return a new rule with the correct padding
     */
    protected NumberRule selectNumberRule(int field, int padding) {
        switch (padding) {
        case 1:
            return new UnpaddedNumberField(field);
        case 2:
            return new TwoDigitNumberField(field);
        default:
            return new PaddedNumberField(field, padding);
        }
    }

    /**
     * <p>
     * Formats a <code>Date</code>, <code>Calendar</code> or <code>Long</code> (milliseconds)
     * object.
     * </p>
     * 
     * @param obj
     *            the object to format
     * @param toAppendTo
     *            the buffer to append to
     * @param pos
     *            the position - ignored
     * @return the buffer passed in
     */
    @Override
    public StringBuffer format(Object obj, StringBuffer toAppendTo, FieldPosition pos) {
        if (obj instanceof Date) {
            return format((Date) obj, toAppendTo);
        } else if (obj instanceof Calendar) {
            return format((Calendar) obj, toAppendTo);
        } else if (obj instanceof Long) {
            return format(((Long) obj).longValue(), toAppendTo);
        } else {
            throw new IllegalArgumentException("Unknown class: "
                    + (obj == null ? "<null>" : obj.getClass().getName()));
        }
    }

    /**
     * <p>
     * Formats a millisecond <code>long</code> value.
     * </p>
     * 
     * @param millis
     *            the millisecond value to format
     * @return the formatted string
     * @since 2.1
     */
    public String format(long millis) {
        return format(new Date(millis));
    }

    /**
     * <p>
     * Formats a <code>Date</code> object.
     * </p>
     * 
     * @param date
     *            the date to format
     * @return the formatted string
     */
    public String format(Date date) {
        Calendar c = new GregorianCalendar(mTimeZone);
        c.setTime(date);
        return applyRules(c, new StringBuffer(mMaxLengthEstimate)).toString();
    }

    /**
     * <p>
     * Formats a milliseond <code>long</code> value into the supplied <code>StringBuffer</code>.
     * </p>
     * 
     * @param millis
     *            the millisecond value to format
     * @param buf
     *            the buffer to format into
     * @return the specified string buffer
     * @since 2.1
     */
    public StringBuffer format(long millis, StringBuffer buf) {
        return format(new Date(millis), buf);
    }

    /**
     * <p>
     * Formats a <code>Date</code> object into the supplied <code>StringBuffer</code>.
     * </p>
     * 
     * @param date
     *            the date to format
     * @param buf
     *            the buffer to format into
     * @return the specified string buffer
     */
    public StringBuffer format(Date date, StringBuffer buf) {
        Calendar c = new GregorianCalendar(mTimeZone);
        c.setTime(date);
        return applyRules(c, buf);
    }

    /**
     * <p>
     * Formats a <code>Calendar</code> object into the supplied <code>StringBuffer</code>.
     * </p>
     * 
     * @param calendar
     *            the calendar to format
     * @param buf
     *            the buffer to format into
     * @return the specified string buffer
     */
    public StringBuffer format(Calendar calendar, StringBuffer buf) {
        if (mTimeZoneForced) {
            calendar.getTimeInMillis(); // / LANG-538
            calendar = (Calendar) calendar.clone();
            calendar.setTimeZone(mTimeZone);
        }
        return applyRules(calendar, buf);
    }

    /**
     * <p>
     * Performs the formatting by applying the rules to the specified calendar.
     * </p>
     * 
     * @param calendar
     *            the calendar to format
     * @param buf
     *            the buffer to format into
     * @return the specified string buffer
     */
    protected StringBuffer applyRules(Calendar calendar, StringBuffer buf) {
        Rule[] rules = mRules;
        int len = mRules.length;
        for (int i = 0; i < len; i++) {
            rules[i].appendTo(buf, calendar);
        }
        return buf;
    }

    /**
     * <p>
     * Parsing is not supported.
     * </p>
     * 
     * @param source
     *            the string to parse
     * @param pos
     *            the parsing position
     * @return <code>null</code> as not supported
     */
    @Override
    public Object parseObject(String source, ParsePosition pos) {
        pos.setIndex(0);
        pos.setErrorIndex(0);
        return null;
    }

    /**
     * <p>
     * Compares two objects for equality.
     * </p>
     * 
     * @param obj
     *            the object to compare to
     * @return <code>true</code> if equal
     */
    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof FastDateFormat)) {
            return false;
        }
        FastDateFormat other = (FastDateFormat) obj;
        if ((mPattern == other.mPattern || mPattern.equals(other.mPattern))
                && (mTimeZone == other.mTimeZone || mTimeZone.equals(other.mTimeZone))
                && (mLocale == other.mLocale || mLocale.equals(other.mLocale))
                && (mTimeZoneForced == other.mTimeZoneForced)
                && (mLocaleForced == other.mLocaleForced)) {
            return true;
        }
        return false;
    }

    /**
     * <p>
     * Returns a hashcode compatible with equals.
     * </p>
     * 
     * @return a hashcode compatible with equals
     */
    @Override
    public int hashCode() {
        int total = 0;
        total += mPattern.hashCode();
        total += mTimeZone.hashCode();
        total += (mTimeZoneForced ? 1 : 0);
        total += mLocale.hashCode();
        total += (mLocaleForced ? 1 : 0);
        return total;
    }

    /**
     * <p>
     * Gets a debugging string version of this formatter.
     * </p>
     * 
     * @return a debugging string
     */
    @Override
    public String toString() {
        return "FastDateFormat[" + mPattern + "]";
    }

    /**
     * Create the object after serialization. This implementation reinitializes the transient
     * properties.
     * 
     * @param in
     *            ObjectInputStream from which the object is being deserialized.
     * @throws java.io.IOException
     *             if there is an IO issue.
     * @throws ClassNotFoundException
     *             if a class cannot be found.
     */
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        init();
    }

    /**
     * <p>
     * Validate that the argument condition is <code>true</code>; otherwise throwing an exception
     * with the specified message. This method is useful when validating according to an arbitrary
     * boolean expression, such as validating a primitive number or using your own custom validation
     * expression.
     * </p>
     * 
     * @param expression
     *            the boolean expression to check
     * @param message
     *            the exception message if invalid
     * @param value
     *            the value to append to the message when invalid if expression is
     *            <code>false</code>
     */
    public static void isTrue(boolean expression, String message, long value) {
        if (!expression) {
            throw new IllegalArgumentException(String.format(message, Long.valueOf(value)));
        }
    }

}
